/*  Implementation of Unix/Linux tail command.
 * 	Output the last part of files. 
 * 	With more than one FILE, precede each with a header giving the file  name.
 *  With no FILE, or when FILE is -, read standard input.

    Copyright (C) 2012  Vasil Kostov <noobed>
    For any questions or advices don't hesitate to contact me - noobed7@gmail.com

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

 */

package tail;

import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.util.LinkedList;

/**
 * This class contains the implementation of Unix/Linux tail command. Supported
 * options are:
 * 
 * --lines (-n) - reading the last lines of file(s) or stdin(if no files are
 * present) or reading from K-th line to the end.
 * 
 * --bytes (-c) - reading the last bytes of file(s) or stdin(if no files are
 * present) or reading from K-th byte to the end.
 * 
 * --follow (-f) - output appended data as the file grows. Following stdin is
 * not supported!
 * 
 * @author Vasil Kostov <noobed>
 * @author noobed7@gmail.com
 * @version 1.0 BETA
 * 
 */

public class TailCommand {

	/**
	 * Outputs the last <i>lines</i> from a file
	 * 
	 * @param lines
	 *            - number of lines to be read
	 * @param raf
	 *            - A reference to an open file. The file must be opened
	 *            correctly and must be readable with the permissions this
	 *            program is started with.
	 * @throws IOException
	 *             If an I/O error occurs.
	 */

	public void readLastLines(long lines, RandomAccessFile raf)
			throws IOException {
		long length = raf.length();

		// buffer's size should be adjustable
		// in next versions
		int bufferSize = 1024;
		byte[] buffer = new byte[bufferSize];

		raf.seek(length - 1);
		char c = (char) raf.read();
		if (c == '\n' || c == '\r')
			lines++;
		if (raf.length() > bufferSize)
			raf.seek(raf.length() - bufferSize);
		else
			raf.seek(0);

		int status = raf.read(buffer);
		raf.seek(raf.length() - status);

		long countedLines = 0;
		long bytesForOffset = 0;
		long currentFilePointer;

		while (true) {
			for (int i = status - 1; i >= 0; i--) {
				if ((char) buffer[i] == '\n')
					countedLines++;

				if (countedLines == lines) {
					retrieveString(bytesForOffset, raf);
					return;
				}
				bytesForOffset++;
			}

			currentFilePointer = raf.getFilePointer();

			if (currentFilePointer == 0 && countedLines < lines) {
				retrieveString(bytesForOffset, raf);
				return;

			} else if (currentFilePointer - bufferSize > 0) {
				raf.seek(currentFilePointer - bufferSize);
				status = raf.read(buffer);

			} else {
				raf.seek(0);
				status = raf.read(buffer);
			}

			raf.seek(raf.length() - status);
		}
	}

	/*
	 * This is method is used by readLastLines and is called with a proper
	 * offset when the number of last lines is found.
	 */
	private void retrieveString(long offset, RandomAccessFile raf)
			throws IOException {
		raf.seek(raf.length() - offset);
		int status;
		while ((status = raf.read()) != -1)
			System.out.print((char) status);
	}

	/**
	 * Outputs lines starting with the K-th of each file.
	 * 
	 * 
	 * @param line
	 *            - represents the K-th line to read from.
	 * @param raf
	 *            - A reference to an open file. The file must be opened
	 *            correctly and must be readable with the permissions this
	 *            program is started with.
	 * @throws IOException
	 *             If an I/O error occurs.
	 */
	public void readFromKthLineToEnd(long line, RandomAccessFile raf)
			throws IOException {
		long countedLines = 2;
		int status;

		while (raf.readLine() != null && countedLines < line)
			countedLines++;
		while ((status = raf.read()) != -1)
			System.out.print((char) status);

	}

	/**
	 * Outputs the last <i>K</i> bytes of a file.
	 * 
	 * @param bytesNumber
	 *            - the last K bytes.
	 * @param raf
	 *            - A reference to an open file. The file must be opened
	 *            correctly and must be readable with the permissions this
	 *            program is started with.
	 * @throws IOException
	 *             If an I/O error occurs.
	 */
	public void readLastBytes(long bytesNumber, RandomAccessFile raf)
			throws IOException {
		if (bytesNumber <= raf.length())
			raf.seek(raf.length() - bytesNumber);
		else
			raf.seek(0);
		int status;
		while ((status = raf.read()) != -1)
			System.out.print((char) status);

	}

	/**
	 * Outputs bytes of a file starting with the K-th.
	 * 
	 * @param startBytes
	 *            - the K-th byte to start with.
	 * @param raf
	 *            - A reference to an open file. The file must be opened
	 *            correctly and must be readable with the permissions this
	 *            program is started with.
	 * @throws IOException
	 *             If an I/O error occurs.
	 */
	public void readFromKthBytesToEnd(long startBytes, RandomAccessFile raf)
			throws IOException {
		if (startBytes - 1 <= raf.length()) {
			raf.seek(startBytes - 1);
			int status;
			while ((status = raf.read()) != -1)
				System.out.print((char) status);
		}
	}

	/**
	 * Outputs lines entered from stdin starting from the K-th.
	 * 
	 * @param lines
	 *            - The number of the K-th line to start printing from.
	 * @param reader
	 *            - A reference to an open file. The file must be opened
	 *            correctly and must be readable with the permissions this
	 *            program is started with.
	 * @throws IOException
	 */
	public void readFromKthLineConsole(long lines, Reader reader)
			throws IOException {
		int countedLines = 1;
		while (((BufferedReader) reader).readLine() != null
				&& countedLines < lines)
			countedLines++;

		int status;
		while ((status = reader.read()) != -1)
			System.out.print((char) status);

	}

	/**
	 * Outputs the last lines from stdin.
	 * 
	 * @param lines
	 *            - The number of last lines the method should return.
	 * @param reader
	 *            - A reference to an open file. The file must be opened
	 *            correctly and must be readable with the permissions this
	 *            program is started with.
	 * @throws IOException
	 *             If an I/O error occurs
	 */
	public void readLastLinesFromConsole(int lines, Reader reader)
			throws IOException {
		StringControl fc = new StringControl(lines);
		String buffer = "";
		int status;

		boolean b = true;
		boolean isNewLine = false;

		while ((status = reader.read()) != -1 || b) {
			if (status != -1) {
				isNewLine = false;
				buffer += (char) status;
				if ((char) status == '\n') {
					fc.push(buffer);
					buffer = "";
					isNewLine = true;
				}

			}
			if (status == -1 && isNewLine == false)
				fc.push(buffer);

			if (status == -1)
				b = false;
		}
		fc.print();
	}

	/**
	 * Outputs the last bytes from stdin starting with the K-th.
	 * 
	 * @param startBytes
	 *            - The number of K-th byte to read from.
	 * @param reader
	 *            - A reference to an open file. The file must be opened
	 *            correctly and must be readable with the permissions this
	 *            program is started with.
	 * @throws IOException
	 *             If an I/O error occurs
	 */
	public void readFromKthByteConsole(long startBytes, Reader reader)
			throws IOException {
		int status;
		long countedBytes = 1;
		while (reader.read() != -1 && countedBytes < startBytes)
			countedBytes++;
		while ((status = reader.read()) != -1)
			System.out.print((char) status);

	}

	/**
	 * Outputs the last bytes from stdin.
	 * 
	 * @param lines
	 *            - The number of last bytes the method should return.
	 * @param reader
	 *            - A reference to an open file. The file must be opened
	 *            correctly and must be readable with the permissions this
	 *            program is started with.
	 * @throws IOException
	 *             If an I/O error occurs
	 */
	public void readLastBytesConsole(int bytesNumber, Reader reader)
			throws IOException {
		CharControl fc = new CharControl(bytesNumber);
		int status;
		while ((status = reader.read()) != -1)
			fc.push((char) status);

		fc.print();
	}

	private Map<String, RandomAccessFile> map;
	private int mapChachedSize;
	private String lastPathname = "";

	/**
	 * This method opens a RandomAccessFile and keeps it live for every valid
	 * file in <i>files</i> More info should be add here.
	 * 
	 * @param files
	 *            - array of String containing files that should be followed
	 * @param numberOfFiles
	 *            - the number of files
	 * @param linesOption
	 *            - indicates if this option is enabled
	 * @param bytesOption
	 *            - indicates if this option is enabled
	 * @param argumentCount
	 *            - the number of argument if there's either <i>linesOption</i>
	 *            or <i>bytesOption</i> is present
	 * @param startFromEnd
	 *            - whether to start following from the beginning of the file or
	 *            the end.
	 * @throws IOException
	 *             If an I/O error occurs.
	 */
	public void tailForever(String[] files, int numberOfFiles,
			boolean linesOption, boolean bytesOption, int argumentCount,
			boolean startFromEnd) throws IOException {

		List<String> l = new LinkedList<String>();
		map = new Hashtable<String, RandomAccessFile>();
		File file;
		RandomAccessFile raf;
		String str;

		if (numberOfFiles > 1)
			System.out.println("==> " + files[0] + " <== ");
		for (int ii = 0; ii < numberOfFiles; ii++) {
			str = files[ii];
			lastPathname = str;
			file = new File(str);
			if (numberOfFiles > 1 && ii > 0)
				System.out.println("\n==> " + str + " <== ");
			if (file.canRead() && file.isDirectory() == false) {
				raf = new RandomAccessFile(file, "r");
				if (linesOption == false && bytesOption == false)
					readLastLines(10, raf);
				else if (linesOption)
					if (!startFromEnd)
						readFromKthLineToEnd(argumentCount, raf);
					else
						readLastLines(argumentCount, raf);
				else if (bytesOption)
					if (!startFromEnd)
						readFromKthBytesToEnd(argumentCount, raf);
					else
						readLastBytes(argumentCount, raf);
				l.add(str);
				map.put(str, raf);
			} else if (file.isDirectory())
				System.out.println("Cannot tail a directory!");

		}
		mapChachedSize = map.size();
		String[] output = new String[l.size()];
		int i = 0;
		for (Iterator<String> it = l.iterator(); it.hasNext(); i++)
			output[i] = (String) it.next();
		useLinuxInotifyStruct(output);
	}

	/*
	 * This method is called from nativeListener and outputs the appended data
	 * to the file. If a file's size has been reduced a message for truncating
	 * is printed and printing begins from the beginning of the file
	 */
	private void tail_something(String pathname) throws IOException {
		// we've already tested pathname and it's all ok
		if (mapChachedSize > 1 && lastPathname.equals(pathname) == false) {
			System.out.println("\n==> " + pathname + " <== ");
			lastPathname = pathname;
		}
		RandomAccessFile raf = map.get(pathname);

		if (raf.getFilePointer() > raf.length()) {
			System.out.println("file : " + pathname + " truncated!");
			raf.seek(0);// closeEverything();return;
		}

		int status;
		while ((status = raf.read()) != -1)
			System.out.print((char) status);
	}

	/*
	 * This is native method. It's used for the option --follow. The
	 * implementation is OS dependent (Unix/Linux). For this purpose has been
	 * used a Kernel inotify API - for more information please visit
	 * http://www.kernel.org/doc/man-pages/online/pages/man7/inotify.7.html
	 */
	private native void useLinuxInotifyStruct(String[] files);

	/*
	 * This method is called from the JNI. It plays a role like a listener who's
	 * being invoked on file change.
	 */
	private void nativeListener(String pathname) {
		if (pathname == null) {
			System.out.println("pathname is NULL");
			System.exit(10);
		}
		try {
			tail_something(pathname);
		} catch (IOException e) {
			System.out.println(e.getMessage());
			System.exit(11);
		}
	}

	/*
	 * This method is accessed through JNI function Its role is to close safely
	 * all opened files for following Assuming there's no memory leak or
	 * something left unclosed return status is 0 - indicating to the OS
	 * everything finished correctly.
	 */
	private void closeEverything() {
		Collection<RandomAccessFile> collec = map.values();
		RandomAccessFile raf;
		for (Iterator<RandomAccessFile> it = collec.iterator(); it.hasNext();) {
			raf = it.next();
			try {
				if (raf != null)
					raf.close();
			} catch (IOException e) {
				System.out.println(e.getMessage());
				System.exit(12);
			}
		}
	}

}
